/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache license, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the license for the specific language governing permissions and
 * limitations under the license.
 */
package cn.ad.server.web.log.log4j.extend;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.rolling.RollingFileManager;
import org.apache.logging.log4j.core.appender.rolling.RolloverFrequency;
import org.apache.logging.log4j.core.appender.rolling.TriggeringPolicy;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Scheduled;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.util.CronExpression;
import org.apache.logging.log4j.core.util.Integers;
import org.apache.logging.log4j.status.StatusLogger;

import java.text.ParseException;
import java.util.Calendar;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 合并了事件时间戳触发，和定时触发功能。
 */
@Plugin(name = "FalconTimeBasedTriggeringPolicy", category = "Core", printObject = true)
@Scheduled
public final class FalconTimeBasedTriggeringPolicy implements TriggeringPolicy {
	private static Logger LOGGER = StatusLogger.getLogger();
	private volatile long nextRolloverMillis;
	private final int interval;
	private final boolean modulate;

	private static final String defaultSchedule = "0 0 0 * * ?";
	private RollingFileManager manager;
	private final CronExpression cronExpression;
	private final Configuration configuration;
	private final AtomicBoolean launched = new AtomicBoolean(false);

	private FalconTimeBasedTriggeringPolicy(final int interval, final boolean modulate, final CronExpression schedule,
			final Configuration configuration) {
		this.interval = interval;
		this.modulate = modulate;
		this.cronExpression = schedule;
		this.configuration = configuration;
	}

	public int getInterval() {
		return interval;
	}

	public long getNextRolloverMillis() {
		return nextRolloverMillis;
	}

	/**
	 * 初始化
	 * 
	 * @param aManager
	 *            The RollingFileManager.
	 */
	@Override
	public void initialize(final RollingFileManager aManager) {
		this.manager = aManager;
		setNextRolloverMillis(aManager.getFileTime());
	}

	/**
	 * 触发.
	 * 
	 * @param event
	 *            A reference to the currently event.
	 * @return true if a rollover should occur.
	 */
	@Override
	public boolean isTriggeringEvent(final LogEvent event) {
		final long nowMillis = event.getTimeMillis();
		if (nowMillis >= nextRolloverMillis) {
			setNextRolloverMillis(nowMillis);
			if (launched.compareAndSet(false, true)) {
				manager.getPatternProcessor().updateTime();
				manager.rollover();
				configuration.getScheduler().scheduleWithCron(cronExpression, new CronTrigger());
			}
			if (manager.getFileSize() == 0) {
				return false;
			}
			return true;
		}
		return false;
	}

	private volatile long timeMarker = 0l;

	/**
	 * 更新时间。
	 * 
	 * @param nowMillis
	 */
	private synchronized void setNextRolloverMillis(long nowMillis) {
		if (nowMillis == -1) {
			if (manager.getFileSize() == 0) {
				return;
			}
			final long gooo = getNextTime(nextRolloverMillis, -interval, modulate);
			manager.getPatternProcessor().getNextTime(gooo, interval, modulate);
			if (manager.getFileSize() == 0) {
				return;
			}
			manager.rollover();
		} else {
			nextRolloverMillis = manager.getPatternProcessor().getNextTime(nowMillis, interval, modulate);
		}
	}

	/**
	 * 得到下时刻
	 * 
	 * @param currentMillis
	 * @param incr
	 * @param mod
	 * @return
	 */
	private long getNextTime(final long currentMillis, final int incr, final boolean mod) {
		RolloverFrequency frequency = manager.getPatternProcessor().getFrequency();

		long nextTime;

		if (frequency == null) {
			throw new IllegalStateException("Pattern does not contain a date");
		}
		final Calendar currentCal = Calendar.getInstance();
		currentCal.setTimeInMillis(currentMillis);
		final Calendar cal = Calendar.getInstance();
		currentCal.setMinimalDaysInFirstWeek(7);
		cal.setMinimalDaysInFirstWeek(7);
		cal.set(currentCal.get(Calendar.YEAR), 0, 1, 0, 0, 0);
		cal.set(Calendar.MILLISECOND, 0);
		if (frequency == RolloverFrequency.ANNUALLY) {
			increment(cal, Calendar.YEAR, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.YEAR, -1);
			return nextTime;
		}
		cal.set(Calendar.MONTH, currentCal.get(Calendar.MONTH));
		if (frequency == RolloverFrequency.MONTHLY) {
			increment(cal, Calendar.MONTH, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.MONTH, -1);
			return nextTime;
		}
		if (frequency == RolloverFrequency.WEEKLY) {
			cal.set(Calendar.WEEK_OF_YEAR, currentCal.get(Calendar.WEEK_OF_YEAR));
			increment(cal, Calendar.WEEK_OF_YEAR, incr, mod);
			cal.set(Calendar.DAY_OF_WEEK, currentCal.getFirstDayOfWeek());
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.WEEK_OF_YEAR, -1);
			return nextTime;
		}
		cal.set(Calendar.DAY_OF_YEAR, currentCal.get(Calendar.DAY_OF_YEAR));
		if (frequency == RolloverFrequency.DAILY) {
			increment(cal, Calendar.DAY_OF_YEAR, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.DAY_OF_YEAR, -1);
			return nextTime;
		}
		cal.set(Calendar.HOUR_OF_DAY, currentCal.get(Calendar.HOUR_OF_DAY));
		if (frequency == RolloverFrequency.HOURLY) {
			increment(cal, Calendar.HOUR_OF_DAY, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.HOUR_OF_DAY, -1);
			return nextTime;
		}
		cal.set(Calendar.MINUTE, currentCal.get(Calendar.MINUTE));
		if (frequency == RolloverFrequency.EVERY_MINUTE) {
			increment(cal, Calendar.MINUTE, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.MINUTE, -1);
			return nextTime;
		}
		cal.set(Calendar.SECOND, currentCal.get(Calendar.SECOND));
		if (frequency == RolloverFrequency.EVERY_SECOND) {
			increment(cal, Calendar.SECOND, incr, mod);
			nextTime = cal.getTimeInMillis();
			cal.add(Calendar.SECOND, -1);
			return nextTime;
		}
		cal.set(Calendar.MILLISECOND, currentCal.get(Calendar.MILLISECOND));
		increment(cal, Calendar.MILLISECOND, incr, mod);
		nextTime = cal.getTimeInMillis();
		cal.add(Calendar.MILLISECOND, -1);
		return nextTime;
	}

	private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) {
		final int interval = modulate ? increment - (cal.get(type) % increment) : increment;
		cal.add(type, interval);
	}

	/**
	 * 创建policy对象
	 * 
	 * @param configuration
	 * @param interval
	 * @param modulate
	 * @param schedule
	 * @return
	 */
	@PluginFactory
	public static FalconTimeBasedTriggeringPolicy createPolicy(@PluginConfiguration final Configuration configuration,
                                                               @PluginAttribute("interval") final String interval, @PluginAttribute("modulate") final String modulate,
                                                               @PluginAttribute("schedule") final String schedule) {
		final int increment = Integers.parseInt(interval, 1);
		final boolean mod = Boolean.parseBoolean(modulate);
		/* 定时器 */
		CronExpression cronExpression;
		if (schedule == null) {
			LOGGER.info("No schedule specified, defaulting to Daily");
			cronExpression = getSchedule(defaultSchedule);
		} else {
			cronExpression = getSchedule(schedule);
			if (cronExpression == null) {
				LOGGER.error("Invalid expression specified. Defaulting to Daily");
				cronExpression = getSchedule(defaultSchedule);
			}
		}
		return new FalconTimeBasedTriggeringPolicy(increment, mod, cronExpression, configuration);
	}

	private class CronTrigger implements Runnable {
		public void run() {
			setNextRolloverMillis(-1);
		}
	}

	/**
	 * 表达式
	 * 
	 * @param expression
	 * @return
	 */
	private static CronExpression getSchedule(final String expression) {
		try {
			return new CronExpression(expression);
		} catch (final ParseException pe) {
			LOGGER.error("Invalid cron expression - " + expression, pe);
			return null;
		}
	}

	@Override
	public String toString() {
		return "FalconTimeBasedTriggeringPolicy(nextRolloverMillis=" + nextRolloverMillis + ", interval=" + interval
				+ ", modulate=" + modulate + ")";
	}
}
